package hudson.plugins.scm_sync_configuration;

import hudson.plugins.scm_sync_configuration.model.ScmContext;
import hudson.plugins.scm_sync_configuration.scms.SCM;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFile;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.command.add.AddScmResult;
import org.apache.maven.scm.command.checkin.CheckInScmResult;
import org.apache.maven.scm.command.checkout.CheckOutScmResult;
import org.apache.maven.scm.command.remove.RemoveScmResult;
import org.apache.maven.scm.command.status.StatusScmResult;
import org.apache.maven.scm.command.update.UpdateScmResult;
import org.apache.maven.scm.manager.NoSuchScmProviderException;
import org.apache.maven.scm.manager.ScmManager;
import org.apache.maven.scm.repository.ScmRepository;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Class providing atomic scm commands and wrapping calls to maven scm api
 * with logging informations
 * @author fcamblor
 */
public class SCMManipulator {

    private static final Logger LOGGER = Logger.getLogger(SCMManipulator.class.getName());

    private final ScmManager scmManager;
    private ScmRepository scmRepository = null;
    private String scmSpecificFilename = null;

    public SCMManipulator(ScmManager _scmManager) {
        this.scmManager = _scmManager;
    }

    /**
     * Will check if everything is settled up (useful before a scm manipulation)
     * @param scmContext
     * @param resetScmRepository
     * @return
     */
    public boolean scmConfigurationSettledUp(ScmContext scmContext, boolean resetScmRepository){
        String scmRepositoryUrl = scmContext.getScmRepositoryUrl();
        SCM scm = scmContext.getScm();
        if(scmRepositoryUrl == null || scm == null){
            return false;
        }

        if(resetScmRepository){
            LOGGER.info("Creating scmRepository connection data ..");
            this.scmRepository = scm.getConfiguredRepository(this.scmManager, scmRepositoryUrl);
            try {
                this.scmSpecificFilename = this.scmManager.getProviderByRepository(this.scmRepository).getScmSpecificFilename();
            }
            catch(NoSuchScmProviderException e) {
                LOGGER.throwing(ScmManager.class.getName(), "getScmSpecificFilename", e);
                LOGGER.severe("[getScmSpecificFilename] Error while getScmSpecificFilename : "+e.getMessage());
                return false;
            }
        }

        return expectScmRepositoryInitiated();
    }

    private boolean expectScmRepositoryInitiated(){
        boolean scmRepositoryInitiated = this.scmRepository != null;
        if(!scmRepositoryInitiated) {
            LOGGER.warning("SCM Repository has not yet been initiated !");
        }
        return scmRepositoryInitiated;
    }

    public UpdateScmResult update(File root) throws ScmException {
        return this.scmManager.update(scmRepository, new ScmFileSet(root));
    }
    public boolean checkout(File checkoutDirectory){
        boolean checkoutOk = false;

        if(!expectScmRepositoryInitiated()){
            return checkoutOk;
        }

        // Checkouting sources
        LOGGER.fine("Checking out SCM files into ["+checkoutDirectory.getAbsolutePath()+"] ...");
        try {
            CheckOutScmResult result = scmManager.checkOut(this.scmRepository, new ScmFileSet(checkoutDirectory));
            if(!result.isSuccess()){
                LOGGER.severe("[checkout] Error during checkout : "+result.getProviderMessage()+" || "+result.getCommandOutput());
                return checkoutOk;
            }
            checkoutOk = true;
        } catch (ScmException e) {
            LOGGER.throwing(ScmManager.class.getName(), "checkOut", e);
            LOGGER.severe("[checkout] Error during checkout : "+e.getMessage());
            return checkoutOk;
        }

        if(checkoutOk){
            LOGGER.fine("Checked out SCM files into ["+checkoutDirectory.getAbsolutePath()+"] !");
        }

        return checkoutOk;
    }

    public List<File> deleteHierarchy(File hierarchyToDelete){
        if(!expectScmRepositoryInitiated()){
            return null;
        }

        File enclosingDirectory = hierarchyToDelete.getParentFile();

        LOGGER.fine("Deleting SCM hierarchy ["+hierarchyToDelete.getAbsolutePath()+"] from SCM ...");

        File commitFile = hierarchyToDelete;
        while(! commitFile.isDirectory()) {
            commitFile = commitFile.getParentFile();
        }

        try {
            ScmFileSet deleteFileSet = new ScmFileSet(enclosingDirectory, hierarchyToDelete);
            StatusScmResult checkForChanges = this.scmManager.status(scmRepository, deleteFileSet);
            LOGGER.fine("Checking for changes on SCM hierarchy ["+hierarchyToDelete.getAbsolutePath()+"] from SCM ...");
            for (ScmFile changedFile : checkForChanges.getChangedFiles()) {
                //check in this change as it affect our hierarchy
                LOGGER.fine("[checkForChanges] Found changed file "+changedFile.toString()+", try to check-in...");
                CheckInScmResult checkedInChangedFile = scmManager.checkIn(scmRepository, new ScmFileSet(enclosingDirectory.getParentFile(), new File(changedFile.getPath())), "Check-In changes for "+changedFile.getPath());
                if(!checkedInChangedFile.isSuccess()){
                  LOGGER.severe("[checkForChanges] Failed to check-in changed file ["+changedFile.getPath()+"]: "+checkedInChangedFile.getProviderMessage());
              }
            }
            RemoveScmResult removeResult = this.scmManager.remove(this.scmRepository, deleteFileSet, "Delete hierarchy "+hierarchyToDelete.getPath());
            if(!removeResult.isSuccess()){
                LOGGER.severe("[deleteHierarchy] Problem during remove : "+removeResult.getProviderMessage());
                return null;
            }

            List<File> filesToCommit = new ArrayList<File>();
            filesToCommit.add(commitFile);

            filesToCommit = refineUpdatedFilesInScmResult(filesToCommit);
            return filesToCommit;
        } catch (ScmException e) {
            LOGGER.throwing(ScmManager.class.getName(), "remove", e);
            LOGGER.severe("[deleteHierarchy] Hierarchy deletion aborted : "+e.getMessage());
            return null;
        }
    }

    public List<File> addFile(File scmRoot, String filePathRelativeToScmRoot){
        List<File> synchronizedFiles = new ArrayList<File>();

        if(!expectScmRepositoryInitiated()){
            return synchronizedFiles;
        }

        LOGGER.fine("Adding SCM file ["+filePathRelativeToScmRoot+"] ...");

        try {
            // Split every directory leading through modifiedFilePathRelativeToHudsonRoot
            // and try add it in the scm
            String[] pathChunks = filePathRelativeToScmRoot.split("\\\\|/");
            StringBuilder currentPath = new StringBuilder();
            for(int i=0; i<pathChunks.length; i++){
                currentPath.append(pathChunks[i]);
                if(i != pathChunks.length-1){
                    currentPath.append(File.separator);
                }
                File currentFile = new File(currentPath.toString());

                // Trying to add current path to the scm ...
                AddScmResult addResult = this.scmManager.add(this.scmRepository, new ScmFileSet(scmRoot, currentFile));
                // If current has not yet been synchronized, addResult.isSuccess() should be true
                if(addResult.isSuccess()){
                    synchronizedFiles.addAll(refineUpdatedFilesInScmResult(addResult.getAddedFiles()));

                    if(i == pathChunks.length-1 && new File(scmRoot.getAbsolutePath()+File.separator+currentPath.toString()).isDirectory()){
                        addResult = this.scmManager.add(this.scmRepository, new ScmFileSet(scmRoot, currentPath.toString()+"/**/*"));
                        if(addResult.isSuccess()){
                            synchronizedFiles.addAll(refineUpdatedFilesInScmResult(addResult.getAddedFiles()));
                        } else {
                            LOGGER.severe("Error while adding SCM files in directory : " + addResult.getCommandOutput());
                        }
                    }
                } else {
                    // If addResult.isSuccess() is false, it isn't an error if it is related to path chunks (except for latest one) :
                    // if pathChunk is already synchronized, addResult.isSuccess() will be false.
                    Level logLevel = (i==pathChunks.length-1)?Level.SEVERE:Level.FINE;
                    LOGGER.log(logLevel, "Error while adding SCM file : " + addResult.getCommandOutput());
                }
            }
        } catch (IOException e) {
            LOGGER.throwing(ScmFileSet.class.getName(), "init<>", e);
            LOGGER.warning("[addFile] Error while creating ScmFileset : "+e.getMessage());
            return synchronizedFiles;
        } catch (ScmException e) {
            LOGGER.throwing(ScmManager.class.getName(), "add", e);
            LOGGER.warning("[addFile] Error while adding file : "+e.getMessage());
            return synchronizedFiles;
        }


        if(!synchronizedFiles.isEmpty()){
            LOGGER.fine("Added SCM files : "+Arrays.toString(synchronizedFiles.toArray(new File[0]))+" !");
        }

        return synchronizedFiles;
    }

    private List<File> refineUpdatedFilesInScmResult(List<?> updatedFiles){
        List<File> refinedUpdatedFiles = new ArrayList<File>();

        // Cannot use directly a List<ScmFile> or List<File> here, since result type will depend upon
        // current scm api version
        for (Object scmFile :updatedFiles) {
            if(scmFile instanceof File){
                String checkoutScmDir = ScmSyncConfigurationBusiness.getCheckoutScmDirectoryAbsolutePath();
                String scmPath = ((File) scmFile).getAbsolutePath();
                if(scmPath.startsWith(checkoutScmDir)){
                    scmPath = scmPath.substring(checkoutScmDir.length() + 1);
                }
                refinedUpdatedFiles.add(new File(scmPath));
            } else if(scmFile instanceof ScmFile){
                refinedUpdatedFiles.add(new File(((ScmFile)scmFile).getPath()));
            } else {
                LOGGER.severe("Unhandled AddScmResult.addedFiles type : " + scmFile.getClass().getName());
            }
        }

        return refinedUpdatedFiles;
    }

    public boolean checkinFiles(File scmRoot, String commitMessage){
        boolean checkinOk = false;

        if(!expectScmRepositoryInitiated()){
            return checkinOk;
        }

        LOGGER.fine("Checking in SCM files ...");

        ScmFileSet fileSet = new ScmFileSet(scmRoot);

        // Let's commit everything !
        try {
            CheckInScmResult result = this.scmManager.checkIn(this.scmRepository, fileSet, commitMessage);
            if(!result.isSuccess()){
                LOGGER.severe("[checkinFiles] Problem during SCM commit : "+result.getCommandOutput());
                return checkinOk;
            }
            checkinOk = true;
        } catch (ScmException e) {
            LOGGER.throwing(ScmManager.class.getName(), "checkIn", e);
            LOGGER.severe("[checkinFiles] Error while checkin : "+e.getMessage());
            return checkinOk;
        }


        if(checkinOk){
            LOGGER.fine("Checked in SCM files !");
        }

        return checkinOk;
    }

    public String getScmSpecificFilename() {
        return scmSpecificFilename;
    }

}
